MVVMでNavigationViewの表示を切り替える方法
はじめに
いきなりですが、
Viewとの通信はデータバインディング機構のような仕組みを通じて行うため、ViewModelの変更は開発者から見て自動的にViewに反映される。
引用元:https://ja.wikipedia.org/wiki/Model_View_ViewModel
さて、MVVMではViewへの表示をDataBindingになるべく任せたいですね。
ですが、Androidには様々なViewが存在します。
DataBindingを使うことで、Viewへの表示がとても簡単になりましたが、一部のViewはそのまま値を渡すことが難しいです。
ex) RecyclerView, NavigationView, TabLayoutなど
今回はNavigationViewへの反映についてご紹介します。
ユーザーのログイン状態でメニューの表示を切り替える
今回のサンプルでは、TwitterKitを利用してログイン中ならユーザー情報を表示させます。
イメージ画像です。
では、コードを見ていきましょう。まずはViewModelです。
ViewModel
class TopViewModel { val mRepository: UserRepository = UserRepository() var user: ObservableField<User> = ObservableField() fun getUser() { launch(UI) { val session = Twitter.getSessionManager().activeSession if (session != null) { val (status, user) = async(context + CommonPool) { mRepository.getUser(session) }.await() when (status) { Status.SUCCESS -> { this@TopViewModel.user.set(user) } } } } } }
getUserでRepositoryからユーザー情報を取得します。
ログインされていない状態では、sessionがnullなので、userも空の状態となります。
また、userをObservableFieldにすることで、値が変化した際にViewに反映されるようにします。
ヘッダーレイアウト
NavigationViewのヘッダー部分のレイアウトです。こちらもDataBindingに表示を任せます。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <data> <variable name="viewModel" type="com.helmos.app.features.top.TopViewModel" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <ImageView android:id="@+id/user_background_image" android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="centerCrop" app:imageUrl="@{viewModel.user != null ? viewModel.user.bannerUrl : ``}" /> <View android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/baseBlack_alpha2" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" > <ImageView android:id="@+id/user_profile_image" android:layout_width="60dp" android:layout_height="60dp" android:layout_marginEnd="8dp" android:scaleType="centerCrop" app:imageUrl="@{viewModel.user != null ? viewModel.user.imageUrlHttps : ``}" /> <TextView android:id="@+id/user_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_toEndOf="@id/user_profile_image" android:text="@{viewModel.user != null ? viewModel.user.name : ``}" android:textColor="@color/baseWhite" android:textStyle="bold" /> <TextView android:id="@+id/user_screen_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/user_name" android:layout_toEndOf="@id/user_profile_image" android:text="@{viewModel.user != null ? `@` + viewModel.user.screenName : ``}" android:textColor="@color/baseWhite" /> </RelativeLayout> </FrameLayout> </layout>
メニュー
NavigationViewのメニュー部分です。いつも通りな感じで。
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/menu_image_search" android:icon="@android:drawable/ic_menu_gallery" android:title="@string/image_search" /> </group> <item android:title="@string/other"> <menu android:checkableBehavior="single"> <item android:id="@+id/menu_license" android:title="@string/license" /> <item android:id="@+id/menu_login" android:title="@string/login" /> <item android:id="@+id/menu_logout" android:title="@string/logout" android:visible="false" /> </menu> </item> </menu>
Activityのレイアウト
NavigationViewの部分のみ記載します。その他はご自由に。
<android.support.design.widget.NavigationView android:id="@+id/main_navigation" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/navigation_header" app:menu="@menu/drawer_menu" app:topNavigation="@{viewModel.user}" />
BindingAdapterを作る
NavigationViewの表示を切り替えるためにBindingAdapterを作ります。
デフォルトのDataBinding機構だと足りない箇所を補ってあげるイメージです。
object NavigationViewBindingAdapter { @JvmStatic @BindingAdapter("bind:topNavigation") fun setupTopNavigation(view: NavigationView, user: User?) { view.menu.findItem(R.id.menu_login).isVisible = user == null view.menu.findItem(R.id.menu_logout).isVisible = user != null } }
BindingAdapterを利用して、メニューのログイン・ログアウトの表示を切り替えます。
まとめ
公式のDataBindingによって、AndroidでもMVVMを採用しやすくなったと感じます。
しかし、HTMLなどと違い、Viewによってはクラスを別に作成する必要があったり(Recyclerなど)と、そのままではバインドできないことが多いです。
そういった箇所で、BindingAdapterを利用して、ViewModelからViewへの反映を自動でさせられるようにしましょう。
ViewModelにViewを持たせて、Viewのメソッドを呼んでしまうと、MVPチックになってしまうので注意しましょう。(体験談)